其他
Android无侵入式主题切换揭示动画,仿Telegram/酷安
https://juejin.cn/user/4156572929374327/posts
private View themeSwitchSunView;
private RLottieDrawable themeSwitchSunDrawable;
代码开始通过从数组中提取参数,设置变量,并为动画准备UI。它获取 drawerLayoutContainer 的宽度和高度,根据 toDark 布尔值设置 darkThemeView 的可见性,并对 drawerLayoutContainer 进行快照(也就是类似截图),以便将其设置给 themeSwitchImageView。 然后代码根据主题是否切换为暗色来设置 themeSwitchImageView 和 themeSwitchSunView。然后将 themeSwitchImageView 设置为之前的位图快照,并使其可见。代码根据用户点击的位置和 drawerLayoutContainer 的尺寸计算出环形揭示动画的最终半径(计算当前点击的控件距离与应用窗口的欧氏距离,看谁大要哪个)。 使用 ViewAnimationUtils#createCircularReveal 创建一个 Animator 对象,该对象将在 drawerLayoutContainer 或 themeSwitchImageView 上执行环形揭示动画,具体取决于主题是否切换为暗色。动画的持续时间设置为 400 毫秒,使用 缓动插值器 实现平滑的动画效果。 最后任务发布到 UI 线程,以在延迟后设置导航栏颜色并检查系统栏颜色。延迟时间是根据主题是否切换为暗色来计算的。最后开始动画。
frameLayout.addView(themeSwitchImageView, 0, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT));
themeSwitchSunView.setVisibility(View.GONE);
} else {
frameLayout.addView(themeSwitchImageView, 1, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT));
themeSwitchSunView.setTranslationX(pos[0] - AndroidUtilities.dp(14));
themeSwitchSunView.setTranslationY(pos[1] - AndroidUtilities.dp(14));
themeSwitchSunView.setVisibility(View.VISIBLE);
themeSwitchSunView.invalidate();
}
https://juejin.cn/post/7300715626817224715
https://pspdfkit.com/blog/2020/change-android-themes-with-circular-reveal-animation/
AppCompatDelegate.MODE_NIGHT_YES // or AppCompatDelegate.MODE_NIGHT_NO
)
Activity.switchToDayMode() // 切换至日间模式
RootView.addView(ImageView) // 给根 View 添加最前端全屏 ImageView
ImageView.setBitmap(screenshot) // ImageView 添加之前的 screenshot
ImageView.animate() // ImageView 进行动画,动画半径由最大 radius 到 0,结束
↓
AppCompatDelegate#applyDayNightToActiveDelegates
↓
AppCompatDelegateImpl#applyDayNight
↓
AppCompatDelegateImpl#applyApplicationSpecificConfig
↓
AppCompatDelegateImpl#updateAppConfiguration
↓
ActivityCompat#recreate
↓
Activity#recreate
↓
ActivityThread#scheduleRelaunchActivity
↓
ActivityThread#scheduleRelaunchActivityIfPossible
↓
ActivityThread#sendMessage
↓
ActivityThread.H#sendMessage
↓
Handler#sendMessage
使用 Handler 的 sendMessage() 或 post() 方法,会将消息添加到 MessageQueue 中。 Looper 不断从 MessageQueue 中检索消息。 当 Looper 检索到消息时,它会将其传递给相应的 Handler。 Handler 的 handleMessage() 方法会处理消息。
screenshot = Window.screenshot() // 先把之前的屏幕截图
Activity.switchToDayMode() // 切换至日间模式
MainHandler.post { // 将下面的消息传递在 recreate 后面
// 此时 Activity 为 新 Activity(日间模式)
RootView.addView(ImageView) // 给根 View 添加最前端全屏 ImageView
ImageView.setBitmap(screenshot) // ImageView 添加之前的 screenshot
ImageView.animate() // ImageView 进行动画,动画半径由最大 radius 到 0,结束
}
↓
ClientTransactionHandler#executeTransaction
↓
...
↓
TransactionExecutor#performLifecycleSequence
↓
ActivityThread#handleRelaunchActivity
↓
ActivityThread#handleRelaunchActivityInner
↓
ActivityThread#performPauseActivity #1
↓
ActivityThread#callActivityOnStop #2
↓
ActivityThread#handleDestroyActivity #3
↓
ActivityThread#performDestroyActivity
↓
ActivityThread#handleLaunchActivity
↓
ActivityThread#performLaunchActivity
↓ ↓
Activity#attach Activity#setTheme #5
↓
Instrumentation#callActivityOnCreate
↓
Activity#performCreate #4
↓
Activity#onCreate
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
/**
* Constructor for main window of an activity.
*/
public PhoneWindow(Context context, Window preservedWindow,
ActivityConfigCallback activityConfigCallback) {
this(context);
// Only main activity windows use decor context, all the other windows depend on whatever
// context that was given to them.
mUseDecorContext = true;
if (preservedWindow != null) {
mDecor = (DecorView) preservedWindow.getDecorView();
mElevation = preservedWindow.getElevation();
mLoadElevation = false;
mForceDecorInstall = true;
// If we're preserving window, carry over the app token from the preserved
// window, as we'll be skipping the addView in handleResumeActivity(), and
// the token will not be updated as for a new window.
getAttributes().token = preservedWindow.getAttributes().token;
}
// Even though the device doesn't support picture-in-picture mode,
// an user can force using it through developer options.
boolean forceResizable = Settings.Global.getInt(context.getContentResolver(),
DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) != 0;
mSupportsPictureInPicture = forceResizable || context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_PICTURE_IN_PICTURE);
mActivityConfigCallback = activityConfigCallback;
}
screenshot = Window.screenshot() // 先把之前的屏幕截图
Activity.switchToNightMode() // 切换至夜间模式,内部是 MainHandler
MainHandler.post { // 将下面的消息传递在 recreate 后面
// 此时 Activity 为 新 Activity(夜间模式)
// 几乎所有之前的字段都不能调用,比如 Window,Activity,因为重建了
// 非要调用最后也没效果
DecorView.addView(ImageView) // 给 DecorView 添加最前端全屏 ImageView
ImageView.setBitmap(screenshot) // ImageView 添加之前的 screenshot
ImageView.animate(onEnd = {
DecorView.removeView(ImageView)
}) // ImageView 进行动画,动画半径由最大 radius 到 0,结束后删除 ImageView
}
screenshot = Window.screenshot() // 先把之前的屏幕截图
Activity.switchToNightMode() // 切换至夜间模式,内部是 MainHandler
MainHandler.post { // 将下面的消息传递在 recreate 后面
// 此时 Activity 为 新 Activity(夜间模式)
// 几乎所有之前的字段都不能调用,比如 Window,Activity,因为重建了
// 非要调用最后也没效果
DecorView.addView(ImageView, 0) // 给 DecorView 添加最末端全屏 ImageView
ImageView.setBitmap(screenshot) // ImageView 添加之前的 screenshot
content = DecorView.findViewById(android.R.id.content) // 次根 View
content.animate(onEnd = {
DecorView.removeView(ImageView)
}) // content 进行动画,动画半径由 0 到最大 radius,结束后删除 ImageView
}
when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
// Do not use event.rawX and event.rawY
// It is not accurate in floating window mode
// x = event.rawX
// y = event.rawY
v.getLocationInWindow(locationInWindow)
x = event.x + locationInWindow[0]
y = event.y + locationInWindow[1]
if (DEBUG) {
Log.d(TAG, "onTouch: x = $x, y = $y")
}
}
}
return false
}
https://github.com/YenalyLiew/CircularRevealSwitch/blob/master/circularrevealswitch/src/main/java/com/yenaly/circularrevealswitch/CircularRevealSwitch.kt